package com.minivision.fdiot.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.domain.Page;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import com.google.common.base.Joiner;
import com.minivision.fastdfs.FdfsService;
import com.minivision.fdiot.common.Action;
import com.minivision.fdiot.common.ChunkRequest;
import com.minivision.fdiot.domain.FaceData;
import com.minivision.fdiot.domain.FaceSetAction;
import com.minivision.fdiot.entity.AppInfo;
import com.minivision.fdiot.entity.Face;
import com.minivision.fdiot.entity.FaceSet;
import com.minivision.fdiot.event.FaceSetUpdateEvent;
import com.minivision.fdiot.event.FaceUpdateEvent;
import com.minivision.fdiot.exception.ErrorType;
import com.minivision.fdiot.exception.FacePlatException;
import com.minivision.fdiot.repository.mysql.AppRepository;
import com.minivision.fdiot.repository.redis.FaceSetRepository;
import com.minivision.fdiot.rest.param.faceset.SetCreateParam;
import com.minivision.fdiot.rest.param.faceset.SetModifyParam;
import com.minivision.fdiot.rest.result.FailureDetail;
import com.minivision.fdiot.rest.result.faceset.AddFaceResult;
import com.minivision.fdiot.rest.result.faceset.RemoveFaceResult;
import com.minivision.fdiot.rest.result.faceset.SetCreateResult;
import com.minivision.fdiot.rest.result.faceset.SetDeleteResult;
import com.minivision.fdiot.rest.result.faceset.SetDetailResult;
import com.minivision.fdiot.rest.result.faceset.SetListResult;
import com.minivision.fdiot.rest.result.faceset.SetMergeResult;
import com.minivision.fdiot.rest.result.faceset.SetModifyResult;
import com.minivision.fdiot.util.BeanPropertyUtils;
import com.minivision.fdiot.util.FeatureUtils;

import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class FaceSetServiceRedis extends BaseService implements FaceSetService, ApplicationEventPublisherAware {

  private static final Logger LOGGER = LoggerFactory.getLogger(FaceSetServiceRedis.class);

  @Autowired
  private FaceSetRepository setRepository;

  @Autowired
  private StringRedisTemplate stringRedisTemplate;

  @Autowired
  private FaceCommonService commonService;
  
  @Autowired
  private FdfsService fdfsService;

  @Value(value = "${face.image.save}")
  private boolean saveImage;

  @Value(value = "${face.image.remove}")
  private boolean removeImage;

  @Autowired
  private AppRepository appRepository;
  
  private ApplicationEventPublisher publisher;
  
  /**
   * 创建人脸库
   */
  public SetCreateResult create(SetCreateParam param)
      throws FacePlatException {
    FaceSet set = new FaceSet();
    BeanUtils.copyProperties(param, set);
    AppInfo appInfo = appRepository.findByAppKey(param.getAppKey());
    if(appInfo == null){
      throw new FacePlatException(ErrorType.APP_NOT_EXIST);
    }
    FaceSet created = setRepository.save(set);
    
    FaceSetAction data = new FaceSetAction();
    data.setFacesetToken(created.getFacesetToken());
    data.setDisplayName(created.getDisplayName());
    data.setCapacity(created.getCapacity());
    data.setPriority(created.getPriority());
    data.setThreshold(created.getThreshold());
    data.setAction(Action.ADD.name());
    //发布人脸库添加事件
    if (param.isNeedPush2Device()) {
      publisher.publishEvent(new FaceSetUpdateEvent(this, param.getAppKey(), data));
    }

    SetCreateResult result = new SetCreateResult();
    result.setFacesetToken(created.getFacesetToken());
    result.setOuterId(created.getOuterId());
    return result;
  }

  /**
   * 添加人脸
   */
  @CacheEvict(cacheNames="features", key="#setToken")
  public AddFaceResult addFace(boolean needPush2Device, String setToken, String appKey, String... faceTokens) throws FacePlatException {
    FaceSet set = commonService.findOneFaceset(setToken);
    checkSetValid(appKey,set);
    int capacity = set.getCapacity();
    int count = stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken)).intValue();
    if (capacity - count < faceTokens.length) {
      LOGGER.error("beyond the capacity of faceset:[{}]", setToken);
      throw new FacePlatException(ErrorType.FACESET_OUT_CAPACITY);
    }

    List<FailureDetail> failureDetail = new ArrayList<>();
    int faceAdded = 0;
    List<FaceData> faceDataList = new ArrayList<>();
    for (String faceToken : faceTokens) {
      try {
        Face added = addFace(setToken, faceToken);
        faceAdded++;
        FaceData faceData = new FaceData();
        faceData.setFacesetToken(setToken);
        faceData.setToken(added.getToken());
        faceData.setFeature(FeatureUtils.encode(added.getFeature()));
        faceDataList.add(faceData);
      } catch (FacePlatException e) {
        log.error("add face failed, setToken:" + setToken + ", faceToken:" + faceToken, e);
        FailureDetail failure = new FailureDetail(faceToken, e.getMessage());
        failureDetail.add(failure);
      }
    }
    
    //发布人脸新增事件
    if (needPush2Device) {
      publisher.publishEvent(new FaceUpdateEvent(this, Action.ADD, appKey, setToken, faceDataList));
    }

    AddFaceResult result = new AddFaceResult();
    result.setFacesetToken(set.getFacesetToken());
    result.setOuterId(set.getOuterId());
    result.setFaceCount(stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken)).intValue());
    result.setFaceAdded(faceAdded);
    if (failureDetail.size() > 0) {
      result.setFailureDetail(failureDetail);
    }
    return result;
  }

  /**
   * 添加一张人脸
   * 
   * @param setToken
   * @param faceToken
   * @throws FacePlatException
   */
  private Face addFace(String setToken, String faceToken) throws FacePlatException {
    Face one = commonService.findOneFaceFromRedis(faceToken);
    if (one == null) {
      one = commonService.findOneFaceFromCache(faceToken);
      if(one == null){
        log.error("face[{}] not exist", faceToken);
        throw new FacePlatException(ErrorType.FACE_NOT_EXIST);
      }
      /*if(saveImage && one.getImg() != null){
        File file = commonService.getFaceImg(faceToken);
        try {
          FileUtils.writeByteArrayToFile(file, one.getImg());
        } catch (IOException e) {
          throw new FacePlatException(ErrorType.SERVER_ERROR);
        }
      }*/
      commonService.saveFeature(one);
    }

    String key = commonService.getFaceSetKey(setToken);
    if (stringRedisTemplate.opsForZSet().rank(key, faceToken) != null) {
      log.error("face[{}] has been added, setToken:{}", faceToken, setToken);
      throw new FacePlatException(ErrorType.FACE_ADD_REPEATED);
    }
    stringRedisTemplate.opsForZSet().add(key, faceToken, 0);
    commonService.addReferenceCnt(one.getToken());
    
    return one;
  }

  /**
   * 删除人脸库
   */
  @Override
  @CacheEvict(cacheNames="features", key="#setToken")
  public SetDeleteResult delete(String setToken, boolean force,String appKey,boolean needPush2Device) throws FacePlatException {
    LOGGER.info("delete faceset:{}", setToken);
    FaceSet faceset = commonService.findOneFaceset(setToken);
    checkSetValid(appKey, faceset);
    if (force) {
      delete(setToken);
    } else {
      if (stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken)).intValue() > 0) {
        throw new FacePlatException(ErrorType.FACESET_NOT_EMPTY);
      } else {
        delete(setToken);
      }
    }
    
    FaceSetAction data = new FaceSetAction();
    data.setFacesetToken(setToken);
    data.setAction(Action.DELETE.name());
    if(needPush2Device){
      publisher.publishEvent(new FaceSetUpdateEvent(this, appKey, data));
    }


    SetDeleteResult result = new SetDeleteResult();
    result.setFacesetToken(setToken);
    result.setOuterId(faceset.getOuterId());
    return result;
  }

  private void checkSetValid(String appKey, FaceSet faceset) throws FacePlatException {
    if(faceset == null){
      throw new FacePlatException(ErrorType.FACE_NOT_EXIST);
    }
    AppInfo appInfo = appRepository.findByAppKey(appKey);
    if(!faceset.getAppKey().equals(appInfo.getAppKey())){
      log.error("faceset appKey:{}, appKey:{}", faceset.getAppKey(), appInfo.getAppKey());
      throw new FacePlatException(ErrorType.FASESET_FORBIDDEN);
    }
  }

  private void delete(String setToken) {
    Set<String> faceTokens = stringRedisTemplate.opsForZSet().rangeByScore(commonService.getFaceSetKey(setToken), 0, 0);
    if (faceTokens != null && !faceTokens.isEmpty()) {
      for (String faceToken : faceTokens) {
        try {
          removeFace(setToken, faceToken);
        } catch (Exception e) {
          log.error("delete faceset[" + setToken + "] to remove face[" + faceToken + "] failed", e);
        }
      }
    }

    stringRedisTemplate.opsForZSet().removeRangeByScore(commonService.getFaceSetKey(setToken), 0, 0);
    setRepository.delete(setToken);
  }

  /**
   * 删除人脸
   */
  @Override
  @CacheEvict(cacheNames="features", key="#setToken")
  public RemoveFaceResult removeFace(boolean needPush2Device, String setToken,String appKey, String... faceTokens) throws FacePlatException {
    LOGGER.info("remove face:setToken-{},faceTokens-{}", setToken, Joiner.on(",").join(faceTokens));
    FaceSet set = commonService.findOneFaceset(setToken);
    checkSetValid(appKey,set);
    List<FailureDetail> failureDetail = new ArrayList<>();
    int faceremoved = 0;
    List<FaceData> faceDataList = new ArrayList<>();
    for (String faceToken : faceTokens) {
      try {
        removeFace(setToken, faceToken);
        faceremoved++;
        FaceData faceData = new FaceData();
        faceData.setFacesetToken(setToken);
        faceData.setToken(faceToken);
        faceDataList.add(faceData);
      } catch (FacePlatException e) {
        log.error("remove face failed, faceToken:" + faceToken, e);
        FailureDetail failure = new FailureDetail(faceToken, e.getMessage());
        failureDetail.add(failure);
      }
    }
    
    //发布人脸删除事件
    if (needPush2Device) {
      publisher.publishEvent(new FaceUpdateEvent(this, Action.DELETE, appKey, setToken, faceDataList));
    }

    RemoveFaceResult result = new RemoveFaceResult();
    result.setFacesetToken(set.getFacesetToken());
    result.setOuterId(set.getOuterId());
    result.setFaceCount(stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken)).intValue());
    result.setFaceRemoved(faceremoved);
    if (failureDetail.size() > 0) {
      result.setFailureDetail(failureDetail);
    }
    return result;
  }

  /**
   * 删除一个人脸
   * @param setToken
   * @param faceToken
   * @throws FacePlatException
   */
  private void removeFace(String setToken, String faceToken) throws FacePlatException {
    String key = commonService.getFaceSetKey(setToken);
    Long removed = stringRedisTemplate.opsForZSet().remove(key, faceToken);
    if (removed == null || removed == 0) {
      log.error("can not remove face, face[{}] not exist in faceset[{}]", faceToken, setToken);
      throw new FacePlatException(ErrorType.FACE_NOT_EXIST);
    }

    int referenceCnt = commonService.decreaseReferenceCnt(faceToken);
    Face one = commonService.findFaceInfo(faceToken);
    if (referenceCnt <= 0 && one != null) {
      commonService.delete(one);
      if (saveImage && removeImage) {
        try {
          fdfsService.deleteFromFastDFS(one.getImgId());
        } catch (Exception e) {
          log.error("delete face img from fastdfs failed, imgUrl:" + one.getImgUrl(), e);
        }
      }
    }
  }

  /**
   * 修改人脸库
   * 
   * @throws FacePlatException
   */
  public SetModifyResult modify(SetModifyParam param) throws FacePlatException {
    LOGGER.info("modify faceset:{}", param);

    FaceSet faceSet = commonService.findOneFaceset(param.getFacesetToken());
    checkSetValid(param.getAppKey(),faceSet);
    BeanPropertyUtils.copyProperties(param, faceSet);
    FaceSet updated = setRepository.save(faceSet);
    
    FaceSetAction data = new FaceSetAction();
    data.setFacesetToken(updated.getFacesetToken());
    data.setDisplayName(updated.getDisplayName());
    data.setCapacity(updated.getCapacity());
    data.setPriority(updated.getPriority());
    data.setThreshold(updated.getThreshold());
    data.setAction(Action.UPDATE.name());
    //发布人脸库更新事件
    if(param.isNeedPush2Device()){
      publisher.publishEvent(new FaceSetUpdateEvent(this, param.getAppKey(), data));
    }

    
    SetModifyResult result = new SetModifyResult();
    result.setFacesetToken(param.getFacesetToken());
    result.setOuterId(faceSet.getOuterId());
    return result;
  }

  /**
   * 查看人脸库信息
   */
  @Override
  public SetDetailResult getFaceSetDetail(String setToken, long offset, long count,String appKey) throws FacePlatException {
    FaceSet faceset = commonService.findOneFaceset(setToken);
    checkSetValid(appKey,faceset);
    SetDetailResult result = new SetDetailResult();
    result.setFacesetToken(faceset.getFacesetToken());
    result.setOuterId(faceset.getOuterId());
    result.setDisplayName(faceset.getDisplayName());
    result.setFaceCount(stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken)).intValue());
    result.setCapacity(faceset.getCapacity());
    result.setPriority(faceset.getPriority());
    result.setThreshold(faceset.getThreshold());
    if(count>0){
      Set<String> faceTokens = stringRedisTemplate.opsForZSet().range(commonService.getFaceSetKey(setToken), offset,
          offset + count - 1);
      result.setFaceTokens(faceTokens);
    }

    return result;
  }

  /**
   * 合并人脸库
   */
  @Override
  @CacheEvict(cacheNames="features", key="#setToken1")
  public SetMergeResult mergeSet(String appKey, String setToken1, String setToken2,boolean needPush2Device) throws FacePlatException {
    LOGGER.info("merge faceset:{},{}", setToken1, setToken2);
    FaceSet faceset1 = commonService.findOneFaceset(setToken1);
    if (faceset1 == null) {
      throw new FacePlatException(ErrorType.FACESET_NOT_EXIST);
    }

    FaceSet faceset2 = commonService.findOneFaceset(setToken2);
    if (faceset2 == null) {
      throw new FacePlatException(ErrorType.FACESET_NOT_EXIST);
    }

    int capacity = faceset1.getCapacity();
    int count1 = stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken1)).intValue();
    int count2 = stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken2)).intValue();
    if (capacity - count1 < count2) {
      LOGGER.error("beyond the capacity of faceset:[{}]", setToken1);
      throw new FacePlatException(ErrorType.FACESET_OUT_CAPACITY);
    }

    Set<String> faceTokens = stringRedisTemplate.opsForZSet().rangeByScore(commonService.getFaceSetKey(setToken2),
        0, 0);

    if (faceTokens != null && !faceTokens.isEmpty()) {
      for (String faceToken : faceTokens) {
        String key = commonService.getFaceSetKey(setToken1);
        if (stringRedisTemplate.opsForZSet().rank(key, faceToken) == null) {
          stringRedisTemplate.opsForZSet().add(key, faceToken, 0);
        } else {
          //stringRedisTemplate.opsForHash().increment("faces:" + faceToken, "referenceCnt", -1);
          commonService.decreaseReferenceCnt(faceToken);
        }
      }
    }

    setRepository.delete(setToken2);
    
    FaceSetAction data1 = new FaceSetAction();
    data1.setFacesetToken(setToken1);
    data1.setAction(Action.MERGE.name());

    //发布人脸库合并事件
    if(needPush2Device){
      publisher.publishEvent(new FaceSetUpdateEvent(this, appKey, data1));
    }

    FaceSetAction data2 = new FaceSetAction();
    data2.setFacesetToken(setToken2);
    data2.setAction(Action.DELETE.name());
    //发布人脸库删除事件
    if(needPush2Device){
      publisher.publishEvent(new FaceSetUpdateEvent(this, appKey, data2));
    }

    SetMergeResult result = new SetMergeResult();
    result.setFacesetToken(faceset1.getFacesetToken());
    result.setOuterId(faceset1.getOuterId());
    result.setDisplayName(faceset1.getDisplayName());
    result.setFaceCount(stringRedisTemplate.opsForZSet().size(commonService.getFaceSetKey(setToken1)).intValue());
    result.setCapacity(faceset1.getCapacity());
    result.setPriority(faceset1.getPriority());
    result.setThreshold(faceset1.getThreshold());

    return result;
  }

  /**
   * 分页获取应用下的人脸库列表
   */
  @Override
  public SetListResult getFaceSetList(int offset, int count,String appKey) throws FacePlatException {
    ChunkRequest chunk = new ChunkRequest(offset, count);
    Page<FaceSet> page = null;
    page = setRepository.findByAppKeyEquals(chunk,appKey);
    SetListResult result = new SetListResult();
    result.setTotal(page.getTotalElements());
    result.setFaceSets(page.getContent());
    return result;
  }

  @Override
  public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
    this.publisher = applicationEventPublisher;
  }

  /**
   * 人脸库是否存在
   */
  @Override
  public boolean exists(String setToken) {
    return setRepository.exists(setToken);
  }

  /**
   * 获取应用下的人脸库列表
   */
  @Override
  public List<FaceSet> findByAppKey(String appKey) {
    return setRepository.findByAppKeyEquals(appKey);
  }
  
}
